# 基本概念

# 语法

# 区分大小写

# 标识符

指变量、函数、属性的名字,或者函数的参数。

  • 第一个字符必须是一个字母、下划线( _ )或一个美元符号( $ );
  • 其他字符可以是字母、下划线、美元符号或数字。
  • 标识符中的字母也可以包含扩展的 ASCII或 Unicode字母字符(如 À和 Æ),但不推荐。
  • 按照惯例,标识符采用驼峰大小写格式,也就是第一个字母小写,剩下的每个单词的首字母大写
  • 不能把关键字、保留字、 truefalsenull 用作标识符。

# 注释

// 单行注释

/*
这是一个多行
(块级)注释
*/
1
2
3
4
5
6

# 严格模式

ECMAScript 5 引入了严格模式(strict mode)的概念。

// 在脚本顶部添加
"use strict";

// 在函数内部的上方指定函数在严格模式下执行:
function doSomething(){
"use strict";
//函数体
}
1
2
3
4
5
6
7
8

# 语句

语句以一个分号结尾;如果省略分号,则由解析器确定语句的结尾

# 关键字、保留字

# 关键字

带 * 号上标的是第 5 版新增的关键字

break do instanceof typeof case else new var catch finally return void continue for switch while debugger* function this with default if throw delete in try

# 保留字

abstract enum int short boolean export interface static byte extends long super char final native synchronized class float package throws const goto private transient debugger implements protected volatile double import public

第 5 版把在非严格模式下运行时的保留字缩减为下列这些:

class enum extends super const export import

在严格模式下,第 5 版还对以下保留字施加了限制:

implements package public interface private static let protected yield

# 变量

ECMAScript 的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说, 每个变量仅仅是一个用于保存值的占位符而已。

var a;
var b = 1;
1
2

函数内定义的变量为局部变量

function test(){
  var a = 1; // 局部变量
}
test();
console.log(a); // 错误!
1
2
3
4
5

函数内省略 var 操作符,可创建一个全局变量(但不推荐)

function test(){
  a = 1; // 省略 var 会声明全局变量,严格模式下会报错 ReferenceError
}
test();
console.log(a); // 1

1
2
3
4
5
6

一条定义多个变量用逗号分隔开即可

var a = 1, b = 2, c;
1

# 数据类型

  • 5 种简单数据类型(也称为基本数据类型): UndefinedNullBooleanNumberString
  • 1种复杂数据类型—— Object

# typeof 操作符

检测变量的数据类型,typeof 是一个操作符而不是函数,因此即使圆括号可以使用,但不是必需的。

var a = 1;
var b = '1';
var c = true;
var d; // 相当于 var d = undefined
var e = null;

console.log(
  typeof a, // number
  typeof b, // string
  typeof c, // boolean
  typeof d, // undefined
  typeof e // object
);

1
2
3
4
5
6
7
8
9
10
11
12
13
14

# Undefined 类型

Undefined 类型只有一个值,即特殊的 undefined

使用 var 声明变量但未对其加以初始化时,自动获得 undefined

var a;
var b = undefined;
console.log(a === b); // true
1
2
3

包含 undefined 值的变量与未定义的变量是不一样的

var a;

console.log(a);
// console.log(b); // 报错,ReferenceError: b is not defined

console.log(typeof b); // undefined, typeof检测未声明的变量返回 undefined

console.log(delete b); // true, delete 操作未声明的变量返回 true,无意义,严格模式会报错
1
2
3
4
5
6
7
8

# Null 类型

Null 类型是第二个只有一个值的数据类型,这个特殊的值是 null

从逻辑角度来看, null 值表示一个空对象指针,而这也正是使用 typeof 操作符检测 null 值时会返回 object 的原因

var a = null;
var b;

console.log(typeof a); // object
console.log(a == b); // true,实际上 undefined 值是派生自 null 值的
console.log(a === b); // false, 值相等但类型不相等
1
2
3
4
5
6

定义的变量准备在将来用于保存对象,那么最好将该变量初始化为 null 而不是其他值。

# Boolean 类型

该类型只有两个字面值: true 和 false 。

var a = true;
var b = false;
1
2

ECMAScript 中所有类型的值都有与这两个 Boolean 值等价的值,可以调用 Boolean() 进行转换。

console.log(
  // number
  Boolean(1), // true
  Boolean(0), // false
  Boolean(NaN), // false

  // string
  Boolean('1'), // true
  Boolean(''), // false

  // undefined
  Boolean(undefined), // false

  // null
  Boolean(null), // false

  // object
  Boolean({}) // true

);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# Number 类型

这种类型使用 IEEE754 格式来表示整数和浮点数值(浮点数值在某些语言中也被称为双精度数值)。

最基本的数值字面量格式是十进制整数,整数还可以通过八进制(以 8 为基数)或十六进制(以 16 为基数)的字面值来表示。

// 十进制
var a = 10;
var a1 = +0;
var a2 = -0;
console.log(a); // 10
console.log(a1 === a2); // true,正零和负零被认为相等


// 八进制,以 0或 0o 或 0O 开头,之后数字为 0~7
// 0 开头时,之后数字大于 7 将忽略开头的 0 ,作为十进制
// 以 0o/0O 开头时,之后数字大于 7 直接报错

var b1 = 010;
var b2 = 0o10;
var b3 = 0O10;

var b4 = 08;
// var b5 = 0O8; //报错 SyntaxError: Invalid or unexpected token
console.log(
  b1, //8
  b2, //8
  b3, //8
  b4 // 8
)

// 十六进制字面值的前两位必须是 0x,后跟任何十六进制数字(0~9 及 A~F)。字母不区分大小写。

var c = 0xA1;
var c1 = 0XA1;
// var c1 = 0XG1; 报错

console.log(
  c, // 161
  c1 // 161
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

在进行算术计算时,所有以八进制和十六进制表示的数值最终都将被转换成十进制数值。

# 浮点数值

浮点数值,就是该数值中必须包含一个小数点,并且小数点后面必须至少有一位数字。虽然小 数点前面可以没有整数,但我们不推荐这种写法。

console.log(
  1.1, // 1.1
  0.1, // 0.1
  .1, // 0.1 不推荐此写法
  1., // 1 小数点后面没有数字——解析为 1
  1.0, // 1 整数——解析为 1
  3.125e7, // 31250000 相当于 3.124 乘以 10 的 7 次方
  0.000000003 // 3e-9,ECMASctipt默认会将小数点后带有 6 个零以上的浮点数值转换为以 e 表示法
)
1
2
3
4
5
6
7
8
9

浮点数值的最高精度是 17 位小数,但在进行算术计算时其精确度远远不如整数。

console.log(0.1 + 0.2) // 0.30000000000000004
1

这是使用基于 IEEE754 数值的浮点计算的通病,ECMAScript 并非独此一家;其他使用相同数值格 式的语言也存在这个问题。因此,永远不要测试某个特定的浮点数值。

# 数值范围

ECMAScript 能够表示的最小值和最大值:

  • 最小数值保存在 Number.MIN_VALUE 中,在大多数浏览器中,这个值是 5e-324

  • 最大数值保存在 Number.MAX_VALUE 中,在大多数浏览器中,这个值是 1.7976931348623157e+308

超出数值范围的则为 Infinity 值。-InfinityNumber.NEGATIVE_INFINITY负无穷)/InfinityNumber.POSITIVE_INFINITY正无穷)。

Infinity 值不能够参与计算。使用 isFinite(num) 函数判断一个数值是否是有限值(true),超出范围则为无限制(false)。

var max = Number.MAX_VALUE;
var min = Number.MIN_VALUE;
var infi = max * 2;
var infi2 = max * -2;
var pInfi = Number.POSITIVE_INFINITY;
var nInfi = Number.NEGATIVE_INFINITY;

console.log(
  max, // 1.7976931348623157e+308
  min, // 5e-324
  infi, // Infinity
  infi2, // -Infinity
  isFinite(infi), // false 不是有限制
  isFinite(infi2), // false 不是有限制
  pInfi, // Infinity
  nInfi, // -Infinity
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# NaN

非数值(Not a Number)是一个特殊的数值。

表示一个本来要返回数值的操作数未返回数值的情况(这样就不会抛出错误了)。

在其他编程语言中,任何数值除以 0 都会导致错误,从而停止代码执行。但在 ECMAScript中, 0 除以 0 才会返回 NaN,正数除以 0 返回 Infinity,负数除以 0返回-Infinity。因此不会影响其他代码的执行。

  • 任何涉及 NaN 的计算都会返回 NaN 。
  • NaN 与任何值都不相等,包括 NaN 本身。

使用 isNaN(param) 函数判断一个参数是否是'非数值'。

isNaN(param) 在接收到一个值之后,会尝试将这个值转换为数值。某些不是数值的值会直接转换为数值,例如字符串 "10" 或 Boolean 值。而任何不能被转换为数值的值都会导致这个函数返回 true 。

isNaN(param)
// 相当于

1
2
3
console.log(
  NaN == NaN, // false
  NaN === NaN, // false
  0 / 0, // NaN
  1 / 0, // Infinity
  -1 / 0, // -Infinity
  isNaN(0/0), // true
  isNaN(1 / 0), // false
  isNaN('abc'), // true 因为 Number('abc) 为 NaN
  isNaN('123'), // false 因为 Number('123) 为 123
  isNaN(true), // false 因为 Number(true) 为 1
  isNaN(undefined), // true 因为 Number(undefined) 为 NaN
  isNaN(null), // false 因为 Number(null) 为 0
  isNaN({}), // true
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

在基于对象调用 isNaN() 函数时,会首先调用对象的 valueOf() 方法,然后确定该方法返回的值是否可以转换为数值。如果不能,则基于这个返回值再调用 toString() 方法,再测试返回值。而这个过程也是 ECMAScript 中内置函数和操作符的一般执行流程

# 数值转换

有 3 个函数可以把非数值转换为数值:

  • Number() 可以用于任何数据类型
  • parseInt() 专门用于把字符串转换成整数。
  • parseFloat() 专门用于把字符串转换成小数。
# Number() / +

使用 Number() 转换时,只要遇到 前后空格/正负号/0b/0o/0x 以外的非数字字符都为NaN

console.log(
  Number(true), // 1
  Number(false), // 0
  Number(123), // 123
  Number(null), // 0
  Number(undefined), // NaN
  
  // 转换 object 首先调用对象的 valueOf() 方法,然后将 valueOf() 返回的值放入 Number()
  // 如果转换的结果是 NaN ,则调用对象的 toString() 方法,然后再按字符串处理
  Number({}), // NaN 首先 Number({}.valueOf()),返回 NaN, 然后 Number({}.toString())
  Number(new String('true')), // NaN

  // 转换字符串
  Number('123'), // 123
  Number(' 123 '), // 123 前后的空格会被忽略
  Number(' 1 23 '), // NaN 中间有空格则无法转换为数值
  Number('-123'), // -123
  Number('+123'), // 123
  Number('0b11'), // 3 // 二进制可转换
  Number('011'), // 11 八进制的前导 0 会被忽略
  Number('0o11'), // 9 // 0o 的八进制写法可转换
  Number('0x11'), // 17 十六进制会成功转换为十进制数值
  Number('1.23'), // 1.23
  Number('01.23'), // 1.23
  Number(''), // 0
  Number('其他字符'), // NaN
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

一元加操作符(+)的操作与 Number() 函数的结果相同。

console.log(
  +true, // 1
  +false, // 0
  +123, // 123
  +null, // 0
  +undefined, // NaN

  // 转换字符串
  +'123', // 123
  + ' 123 ', // 123
  + ' 1 23 ', // NaN
  +'-123', // -123
  +'+123', // 123
  +'011', // 11
  +'0x11', // 17
  +'1.23', // 1.23
  +'01.23', // 1.23
  +'', // 0
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# parseInt(param, 2|8|10|16)

第一个参数为需要转换的参数,第二个参数可以指定参数为多少进制。

// ???
parseInt(param)
// 相当于
parseInt(String(param))
1
2
3
4

不指定第二个参数的情况下,ES5 开始忽略八进制字符串的前导0

console.log(
  parseInt(true), // NaN
  parseInt(false), // NaN
  parseInt(123), // 123
  parseInt(null), // NaN
  parseInt(undefined), // NaN
  
  parseInt({}), // NaN
  parseInt(new String('true')), // NaN

  // 转换字符串
  parseInt('123'), // 123
  parseInt('a123'), // NaN // 第一个字符就无法转换为数值
  parseInt('123a'), // 123 // 返回遇到的第一个非数字之前的数值
  parseInt('1a2a3a'), // 1 // 同上
  parseInt( ' 123 '), // 123 // 忽略前后空格
  parseInt( ' 1 23 '), // 1 // 忽略前后空格,中间遇到空格停止转换
  parseInt('-123'), // -123 // 正负号原样返回
  parseInt('+123'), // 123 // 正负号原样返回
  parseInt('0b11'), // 0 // 二进制无法转换,遇到 b 停止转换
  parseInt('011'), // 11 // 忽略八进制的前导  0
  parseInt('0o11'), // 0 // 忽略八进制的 0o 遇到 o 停止转换
  parseInt('0x11'), // 17 // 十六进制可以转换,返回十进制
  parseInt('1.23'), // 1 // 遇到小数点停止转换
  parseInt('001.23'), // 1 // 忽略前置 0
  parseInt(''), // NaN // 空字符串转为 NaN
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

如果需要转换10进制以外的数值,应当指定第二个参数,指定第二个参数后,字符串中0b/0o/0x可省略

实际上,尽管多数情况要解析的都是十进制数值,但始终将 10 作为第二个参数也是非常必要的。

console.log(
  parseInt('11', 2), // 3
  parseInt('11', 8), // 9
  parseInt('11', 10), // 11
  parseInt('11', 16), // 17
)

1
2
3
4
5
6
7
# parseFloat()

parseInt()一样,从第一个字符(位置 0)开始解析每个字符。一直到最后一个字符,除非遇见一个无效的浮点数字字符为止

parseInt()区别:

  • 字符串中的第一个小数点是有效的,而第二个小数点就是无效的了,因此它后面的字符串将被忽略。
  • parseFloat() 只解析十进制值,因此它没有用第二个参数指定基数的用法。
  • 如果字符串包含的是一个可解析为整数的数(没有小数点,或者小数点后都是零), parseFloat() 会返回整数。
console.log(
  parseFloat('123'), // 123
  parseFloat('123.0'), // 123
  parseFloat('123.456'), // 123.456
  parseFloat('123.45.6'), // 123.45
  parseFloat(' +123.456 '), // 123.456
  parseFloat(' -123.456 '), // -123.456
  parseFloat('0b11'), // 0
  parseFloat('0o11'), // 0
  parseFloat('0x11'), // 0
)
1
2
3
4
5
6
7
8
9
10
11

# String 类型

表示由零或多个 16 位 Unicode 字符组成的字符序列。可以由双引号("")或单引号('')表示。

单双引号使用没有区别完全一样,但必须配对使用,不能单双引号混搭。

var a = 'hello';
var a = "hello";
var a = "hello'; // 错误
1
2
3

# 字符字面量

String 数据类型包含一些特殊的字符字面量,也叫转义序列,用于表示非打印字符,或者具有其 他用途的字符。

字面量 含义
\n 换行
\t 制表
\b 退格
\r 回车
\f 进纸
\ 斜杠
' 单引号( ' ),在用单引号表示的字符串中使用。例如: 'He said, 'hey.''
" 双引号( " ),在用双引号表示的字符串中使用。例如: "He said, "hey.""
\xff 以十六进制代码 ff 表示的一个字符(0~F)。例如, \x41 表示 "A"
\uffff 以十六进制代码 ffff 表示的一个Unicode字符(0~F)。例如, \u03a3 表示希腊字符Σ

这些字符字面量可以出现在字符串中的任意位置,而且也将被作为一个字符来解析

# length 属性

任何字符串的长度都可以通过访问其 length 属性取得

console.log('hello\nworld'.length) // 11 \n为一个字符
1

这个属性返回的字符数包括 16位字符 的数目。如果字符串中包含双字节字符,那么 length 属性可能不会精确地返回字符串中的字符数目。

# 字符串的特点

ECMAScript 中的字符串是不可变的,也就是说,字符串一旦创建,它们的值就不能改变。

要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个包含新值的字符串填充该变量,这个过程是在后台发生的。

'use strict'

var a = 'hi';
// a[1] = 'a'; // TypeError: Cannot assign to read only property '1' of string 'hi'
a = 'ha';
console.log(a) // ha;
1
2
3
4
5
6

# 转换为字符串

要把一个值转换为一个字符串有三种方式。

  • 使用 toString() 方法转换
  • 使用转型函数 String()
  • 使用加号操作符(+)与一个空字符串( '' )加在一起。
# toString()
var bool = true;
var num = 123;
var obj = {
  a: 1
};
var str = 'hello';
var undef = undefined;
var nu = null;

console.log(
  // 数值、布尔值、对象和字符串值都有 toString() 方法。
  bool.toString(), // 'true'
  
  // 在调用 数值 的 toString() 方法时,可指定参数表示要输出多少进制类型的字符串
  num.toString(), // '123'
  num.toString(2), // '1111011'
  num.toString(8), // '173'
  num.toString(10), // '123'
  num.toString(16), // '7b'
  obj.toString(), // '[object Object]'
  str.toString(), // 'hello'

  // null 和 undefined 值没有这个方法。
  // nu.toString(), // TypeError: Cannot read property 'toString' of null
  // undef.toString(), // TypeError: Cannot read property 'toString' of undefined
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# String()
var bool = true;
var num = 123;
var obj = {
  a: 1
};
var str = 'hello';
var undef = undefined;
var nu = null;

console.log(
  // 如果参数有 `toString()` 方法,则调用该方法(没有参数)并返回相应的结果;
  String(bool), // 'true'
  String(num), // '123'
  String(obj), // '[object Object]'
  String(str), // 'hello'
  // 如果参数是 undefined ,则返回 "undefined" 。
  String(undef), // 'undefined'
  // 如果参数是 null ,则返回 "null" ;
  String(nu), // 'null'
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# xxx + '' 转换为字符串
var bool = true;
var num = 123;
var obj = {
  a: 1
};
var str = 'hello';
var undef = undefined;
var nu = null;

console.log(
  bool + '', // 'true'
  num + '', // '123'
  obj + '', // '[object Object]'
  str + '', // 'hello'
  undef + '', // 'undefined'
  nu + '', // 'null'
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Object 类型

ECMAScript 中的对象其实就是一组数据和功能的集合。对象可以通过执行 new 操作符后跟要创建 的对象类型的名称来创建。

var obj = new Object();
var obj = new Object; // 不传递参数可省略圆括号,但不推荐。
1
2

Object 类型是所有它的实例的基础。也就是说,Object 类型所具有的任何属性和方法也同样存在于更具体的对象中。

Object 的每个实例都具有下列属性和方法。

  • constructor() :保存着用于创建当前对象的函数。对于前面的例子而言,构造函数(constructor)就是 Object() 。
  • hasOwnProperty(propertyName) :用于检查给定的属性在当前对象实例中(而不是在实例 的原型中)是否存在。其中,作为参数的属性名( propertyName )必须以字符串形式指定(例 如: o.hasOwnProperty("name") )。
  • isPrototypeOf(object) :用于检查传入的对象是否是传入对象的原型(第 5 章将讨论原 型)。
  • propertyIsEnumerable(propertyName) :用于检查给定的属性是否能够使用 for-in 语句 (本章后面将会讨论)来枚举。与 hasOwnProperty() 方法一样,作为参数的属性名必须以字符 串形式指定。
  • toLocaleString() :返回对象的字符串表示,该字符串与执行环境的地区对应。
  • toString() :返回对象的字符串表示。
  • valueOf() :返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值相同。

由于在 ECMAScript 中 Object 是所有对象的基础,因此所有对象都具有这些基本的属性和方法。

之后将具体学习 Object 与其他对象的关系。

# 高级知识补充:Object 类型转原始类型(String/Number/Boolean

对象转为原始类型只有三种:字符串,数值,布尔值

对象转原始类型会涉及到三个方法

Object.prototype.[Symbol.toPrimitive]() // 最先执行,在此函数中会去按条件执行 valueOf() 和 toString()
Object.prototype.toString() // 通常返回字符串类似于 '[object Object]' '[object Array]' 等
Object.prototype.valueOf() // 通常返回对象自身
1
2
3

Object.prototype.[Symbol.toPrimitive]() 规则:

如果实例 obj 是原始类型的包装对象(如 new Number(1)),则返回当前值;

如果 objString,则先调用 obj.toSting()方法,若该方法返回的原始类型 则直接转字符串返回,否则再调用 obj.valueOf()方法并将返回结果转为字符串返回;

如果 objNumber|Boolean,则先调用 obj.valueOf() 方法, 若该方法返回的原始类型则直接转数值返回,否则再调用 obj.toString()方法并返回结果;

如果都没有 原始类型 返回,则抛出 TypeError 类型错误。

# 1. Object => String
String(obj)

// 实际上首先会执行
String(obj.toString())
// 如果 obj.toString() 返回原始类型(若未重写通常会返回字符串类似于 '[object Object]'),则会执行 String()
// 如果 obj.toString() 返回的不是原始类型(被重写后可能返回的不是原始类型),继续调用 valueOf() 方法
String(obj.valueOf()) // 将 obj.valueOf() 的返回结果转为字符串返回
1
2
3
4
5
6
7
var obj1 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  }
}
var obj2 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  valueOf () {
    return this.arr[1]
  }
}
var obj3 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  },
  valueOf () {
    return this.arr[1]
  }
}
var obj4 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr
  },
  valueOf () {
    return this.arr[1]
  }
}
var obj5 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr
  },
  valueOf () {
    return this.arr
  }
}
console.log(
  // 执行 toString() 返回原始类型 数值 1,然后直接执行 String()
  String(obj1), // '1'

  // 执行 toString() 返回原始类型字符串'[object Object]',不会再执行 valueOf(),直接执行外层 String()
  String(obj2), // '[object Object]'

  // 执行 toString() 返回原始类型 数值 1,不会再执行 valueOf(),直接执行外层 String()
  String(obj3), // '1'

  // 执行 toString() 返回非原始类型 数组对象,继续执行 valueOf() 返回 '2',然后再执行外层 String()
  String(obj4), // '2'

  // 执行 toString() 返回数组对象,继续执行 valueOf() 也返回数组对象,执行外层 String()无法转换,报错
  String(obj5), // 报错 TypeError: Cannot convert object to primitive value
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 2. Object => Number | Boolean
Number(obj)
// 实际上首先会执行
Number(obj.valueOf())
// 如果 obj.valueOf() 返回原始类型(重写后返回的原始类型,不重写通常返回对象自身),则会执行 Number()
// 如果 obj.valueOf() 返回的不是原始类型,继续调用 toString() 方法
Number(obj.toString())// 将 obj.valueOf() 的返回结果执行 Number() 返回 (数值或 NaN )
1
2
3
4
5
6
var obj1 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  }
}
var obj2 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  valueOf () {
    return this.arr[1]
  }
}
var obj3 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  },
  valueOf () {
    return this.arr[1]
  }
}
var obj4 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  },
  valueOf () {
    return this.arr
  }
}
var obj5 = {
  subObj: { a: 1 },
  arr: [1, 2, 3],
  toString () {
    return this.arr
  },
  valueOf () {
    return this.arr
  }
}

console.log(
  // 执行 valueOf() 返回对象自身,继续执行 toString() 返回 '1',然后再执行外层 Number()
  Number(obj1), // '1'

  // 执行 valueOf() 返回数值 2,不会再执行 toString(),直接执行外层 Number()
  Number(obj2), // '2'

  // 执行 valueOf() 返回数值 2,不会再执行 toString(),直接执行外层 Number()
  Number(obj3), // '1'

  // 执行 valueOf() 返回一个数组对象,继续执行 toString() 返回 '1',然后再执行外层 Number()
  Number(obj4), // '1'

  // 执行 valueOf() 返回数组对象,继续执行 toString() 也返回数组对象,执行外层 Number()无法转换,报错
  Number(obj5), // 报错 TypeError: Cannot convert object to primitive value
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 3. ES6 中 [Symbol.toPrimitive]() 方法调用优先级比 toString() valueOf() 更高

当执行将对象转换为原始类型的操作时(String()/Number()/Boolean()/比较运算符等操作),程序会优先调用 [Symbol.toPrimitive]() 方法,然后再根据要转换的类型,决定toString()/valueOf()的调用顺序。

所以,除了重写 toString()valueOf() 方法外,还可以直接重写 [Symbol.toPrimitive]()方法

var obj = {
  arr: [1, 2, 3],
  [Symbol.toPrimitive] () {
    return this.arr
  }
}
var obj2 = {
  arr: [1, 2, 3],
  [Symbol.toPrimitive] () {
    return this.arr[0]
  }
}
var obj3 = {
  arr: [1, 2, 3],
  toString () {
    return this.arr[0]
  },
  [Symbol.toPrimitive] () {
    return this.toString()
  }
}
var obj4 = {
  arr: [1, 2, 3],
  valueOf () {
    return this.arr[1]
  },
  [Symbol.toPrimitive] () {
    return this.valueOf()
  }
}
var obj5 = {
  arr: [1, 2, 3],
  valueOf () {
    return this.arr[1]
  },
  toString () {
    return this.arr
  },
  [Symbol.toPrimitive] () {
    let toString = this.toString()
    return toString !== null && !(typeof toString).includes('object') ? toString : this.valueOf()
  }
}
var obj6 = {
  arr: [1, 2, 3],
  valueOf () {
    return this.arr
  },
  toString () {
    return this.arr[0]
  },
  [Symbol.toPrimitive] () {
    let valueOf = this.valueOf()
    return valueOf !== null && !(typeof valueOf).includes('object') ? valueOf : this.toString()
  }
}

console.log(
  // Number(obj) // 报错 TypeError: Cannot convert object to primitive value
  Number(obj2), // 1
  Number(obj3), // 1
  Number(obj4), // 2
  Number(obj5), // 2
  Number(obj6), // 1
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 4. Object 在比较运算符中的隐式转换
var obj = {
  toString () {
    return 1
  },
  valueOf () {
    return 2
  }
}

console.log(obj == 1); // false
console.log(obj == 2); // true
console.log(obj == '1'); // false
console.log(obj == '2'); // true

console.log(obj === 1); // false
console.log(obj === 2); // false
console.log(obj === '1'); // false
console.log(obj === '2'); // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

由此可见,Object 在比较运算符中的隐式转换先调用 valueOf() ,再调用 toString()

# 操作符

包括算术操作符(如加号和减号)、位操作符、关系操作符和相等操作符。

ECMAScript 操作符的与众不同之处在于,它们能够适用于很多值,例如字符串、数字值、布尔值,甚至对象。

在应用于对象时,相应的操作符通常都会调用对象的 valueOf()和(或) toString() 方法,以便取得可以操作的值。

# 一元操作符

只能操作一个值的操作符。

# 递增(++)和递减(--)操作符

均有两个版本:前置型和后置型。

语法:

++a;
// 相当于
a = Number(a) + 1;

--a;
// 相当于
a = Number(a) - 1;
1
2
3
4
5
6
7

++a/--aa++/a-- 的区别在于前者先将变量值改了再用于当前语句中,后者先将原值用于语句中执行语句后,再执行更改变量的操作。但最终更改后的变量的值是完全相同的。

var a = 30;
var b = 30;
var c = 30;
var d = 30;

console.log(
  ++a, // 31
  b++, // 30
  --c, // 29
  d-- // 30
)

console.log(
  a, // 31
  b, // 31
  c, // 29
  d // 29
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

对任何值都适用,可用于整数、字符串、布尔值、浮点数值和对象。

var a = '30';
var b = '30a';
var c = true;
var d = 1.1;
var e = {}

console.log(
  ++a, // 31 // 相当于 a = Number(a) + 1 再返回 a,以下同理
  ++b, // NaN
  ++c, // 2
  --d, // 0.10000000000000009(由于浮点舍入错误所致)

  // 对象:先执行 Number(e.valueOf())若为 NaN,再执行 Number(e.toString()),最后执行++
  ++e, // NaN
)

// 执行后的变量都变为 number 类型
console.log(
  a, // 31
  b, // NaN
  c, // 2
  d, // 0.10000000000000009(由于浮点舍入错误所致)
  e, // NaN
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 一元加(+)和减(-)操作符

一元加操作符(+),放在数值前面,对数值不会产生任何影响。 一元加操作符(+),放在非数值前面,会对它执行 Number()转换操作。

console.log(
  +11, // 11 无任何影响
  +'123', // 123
  +true, // 1
  +undefined, // NaN
  +null // 0
)
1
2
3
4
5
6
7

一元减操作符(-)主要用于表示负数。应用于数值时,该值会变成负数。应用于非数值时,先执行 Number()转换。再转为负数。

var a = 1;
console.log(
  -a, // -1
  -'123', // -123 遇到非数值类型都会先对它执行 `Number()`转换操作。再转为负数
)
console.log(a) // 1
1
2
3
4
5
6

# 位操作符

位操作符用于在最基本的层次上,即按内存中表示数值的位来操作数值。

ECMAScript 中的所有数值都以 IEEE-754 64 位格式存储,但位操作符并不直接操作 64 位的值。而是先将 64 位的值转换成 32 位的整数,然后执行操作,最后再将结果转换回 64 位。但这个转换过程也导致了一个严重的副效应,即在对特殊的 NaN 和 Infinity 值应用位操作时,这两个值都会被当成 0 来处理。

对于有符号的整数,32 位(数值的位从右往左数,下同)中的前 31 位用于表示整数的值。第 32 位用于表示数值的符号:0 表示正数,1 表示负数。这个表示符号的位叫做符号位,符号位的值决定了其他位数值的格式。

其中,正数以纯二进制格式存储,31 位中的每一位都表示 2 的幂。第一位(叫做位 0)表示 2 的 0 次方,第二位表示 2 的 1 次方 ,以此类 推 。 没有用到的位以 0 填充 , 即忽略不计 。

例如,数值 18 的二进制表示是 00000000000000000000000000010010,或者更简洁的 10010 。这是 5 个有效位,这 5 位本身就决定了实际的值。

负数同样以二进制码存储,但使用的格式是二进制补码。计算一个数值的二进制补码,需要经过下 列 3 个步骤:

  1. 求这个数值绝对值的二进制码(例如,要求18 的二进制补码,先求 18 的二进制码);
  2. 求二进制反码,即将 0 替换为 1,将 1 替换为 0;
  3. 得到的二进制反码加 1。

根据这 3 个步骤求得-18 的二进制码

0000 0000 0000 0000 0000 0000 0001 0010 # 首先就要求得 18 的二进制码
1111 1111 1111 1111 1111 1111 1110 1101 # 然后,求其二进制反码,即 0 和 1 互换:

1111 1111 1111 1111 1111 1111 1110 1101
                                      +
                                      1
---------------------------------------
1111 1111 1111 1111 1111 1111 1110 1110 # 最后,二进制反码加 1:
1
2
3
4
5
6
7
8

这样,就求得了 -18 的二进制表示,即 11111111111111111111111111101110。

要注意的是,在处理有符号整数时,是不能访问第32位(位31) 的。

ECMAScript 会尽力向我们隐藏所有这些信息。换句话说,在以二进制字符串形式输出一个负数时, 我们看到的只是这个负数绝对值的二进制码前面加上了一个负号。

var num = -18;
console.log(num.toString(2)); // '-10010'
1
2

默认情况下,ECMAScript 中的所有整数都是有符号整数。不过,当然也存在无符号整数。对于无符号整数来说,第 32 位不再表示符号,因为无符号整数只能是正数。而且,无符号整数的值可以更大,因为多出的一位不再表示符号,可以用来表示数值。

对非数值应用位操作符,会先对它执行 Number() 函数。

# ~ 按位非(NOT)

执行按位非的结果就是返回数值的二进制的反码(1 变为 00 变为 1 )。

var num1 = 25; // 二进制 00000000000000000000000000011001
var num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
1
2
3

对 25 执行按位非操作,结果得到了 -26。这也验证了按位非操作的本质:操作数的负值减 1。

下面的代码也能得到相同的结果:

var num1 = 25;
var num2 = -num1 - 1;
console.log(num2); // -26
1
2
3

虽然以上代码也能返回同样的结果,但由于按位非是在数值表示的最底层执行操作,因此速度更快。

# & 按位与(AND)

& 操作符操作两个数值。

它将两个数值的二进制每一位对齐比较,返回一个新的32位二进制数:相同位上均为 1 则返回 1 ,否则返回 0

console.log(
  25 & 3 // 1
)

/*
// 底层实现: 两个数只有第一位均为 1,返回的 32 位二进制也就只有第一位为 1
0000 0000 0000 0000 0000 0000 0001 1001 // 25
0000 0000 0000 0000 0000 0000 0000 0011 // 3
---------------------------------------
0000 0000 0000 0000 0000 0000 0000 0001 // 32位二进制结果,表现为十进制为 1
*/
1
2
3
4
5
6
7
8
9
10
11

# | 按位或(OR)

| 操作符与 & 操作符一样, 同样对比两个数值的二进制位,但只要有一边为 1 ,则可返回 1

console.log(
  25 | 3 // 27
)

// 底层实现: 两个数只有第一位均为 1,返回的 32 位二进制也就只有第一位为 1
0000 0000 0000 0000 0000 0000 0001 1001 // 25
0000 0000 0000 0000 0000 0000 0000 0011 // 3
---------------------------------------
0000 0000 0000 0000 0000 0000 0001 1011 // 32位二进制结果,表现为十进制为 27
1
2
3
4
5
6
7
8
9

# ^ 按位异或(XOR)

^操作符同样对比两个数值的二进制位,但两边相同(均为 0 或均为 1 )时为 0 ,不同(一边为 0,一边为 1 )时才为 1

console.log(
  25 | 3 // 26
)

/*
// 底层实现: 两个数只有第一位均为 1,返回的 32 位二进制也就只有第一位为 1
0000 0000 0000 0000 0000 0000 0001 1001 // 25
0000 0000 0000 0000 0000 0000 0000 0011 // 3
---------------------------------------
0000 0000 0000 0000 0000 0000 0001 1010 // 32位二进制结果,表现为十进制为 26
*/
1
2
3
4
5
6
7
8
9
10
11

# << 按位左移

操作符会将数值的所有二进制位向左移动指定的位数,最右侧补 0

console.log(
  2 << 5, // 64
  -2 << 5, // -64
)

// 2 的 二进制位 10
// 向左移动 5 位为 1000000
// 十进制则为 64
1
2
3
4
5
6
7
8

注意,左移不会影响操作数的符号位。

# >> 有符号的右移

操作符会将数值的二进制位向右移动,但保留符号位(即正负号标记),最左侧(第31位处,位30)开始补 0

console.log(
  64 >> 5, // 2
  -64 >> 5, // -2
)

// 64 的 二进制位 1000000
// 向右移动 5 位为 10
// 十进制则为 64
1
2
3
4
5
6
7
8

# >>> 无符号右移

操作符会忽略正负号,将数值的所有 32 位都向右移动。最左侧补 0 。对正数来说没有区别。负数有区别。

console.log(
  64 >>> 5, // 2
  -64 >>> 5, // 134217726
)

/*
0000 0000 0000 0000 0000 0000 0100 0000 // 64 的二进制
0000 0000 0000 0000 0000 0000 0000 0010 // 无符号右移 5 位之后的二进制,十进制则为 2


0000 0000 0000 0000 0000 0000 0100 0000 // 64 的二进制
1111 1111 1111 1111 1111 1111 1011 1111 // 64 的二进制反码
1111 1111 1111 1111 1111 1111 1100 0000 // 64 的二进制反码 + 1 得到 -64 的二进制
0000 0111 1111 1111 1111 1111 1111 1110 // 无符号右移 5 位的二进制,十进制为 134217726
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 布尔操作符

布尔操作符一共有 3 个:

  • ! 非(NOT)
  • && 与(AND)
  • || 或(OR)。

# ! 逻辑非

逻辑非操作符首先会将它的操作数转换为一个布尔值,然后再对其求反。

! 只有后面跟着 falsy 值才返回 true,其余全部返回 false

console.log(
  !123, // false // 相当于 !Boolean(123) => !true => false
)
1
2
3

同时使用两个逻辑非操作符,将和 Boolean() 转型函数的效果一样。

console.log(
  !!123 // true // 相当于 !!Boolean(123) => !!true => !false => true
)

1
2
3
4

# && 逻辑与

&& 操作符操作两个值

左侧先执行 Boolean(),为 true 则返回右侧值,为 false 则直接返回左侧值,停止解析右侧(短路)。

console.log(
  false && 1, // false
  1 && false, // false

  0 && 1, // 0
  1 && 0, // 0

  '' && 1, // ''
  1 && '', // ''

  undefined && 1, // undefined
  1 && undefined, // undefined

  null && 1, // null
  1 && null, // null

  NaN && 1, // NaN
  1 && NaN, // NaN

  {} && 1, // 1
  1 && {}, // {}
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# || 逻辑或

|| 操作符操作两个值。

左侧先执行 Boolean(),为 true 则直接返回左侧,停止解析执行右侧(短路)。为 false 才返回右侧值。

console.log(
  false || 1, // 1
  1 || false, // 1

  0 || 1, // 1
  1 || 0, // 1

  '' || 1, // 1
  1 || '', // 1

  undefined || 1, // 1
  1 || undefined, // 1

  null || 1, // 1
  1 || null, // 1

  NaN || 1, // 1
  1 || NaN, // 1

  {} || 1, // {}
  1 || {}, // 1
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 乘性操作符

  • 乘法(*
  • 除法(/
  • 求模(求余数)(%

如果参与乘性计算的某个操作数不是数值,会先对它执行 Number() 转型函数将其转换为数值。

# 乘法(*

  • 如果乘积超过了 ECMAScript 数值的表示范围,则返回 Infinity-Infinity
  • 如果有一个操作数是 NaN ,则结果是 NaN
  • 如果是 Infinity0 相乘,则结果是 NaN
  • 如果是 Infinity 与非 0 数值相乘,则结果是 Infinity-Infinity ,取决于有符号操作数的符号;
  • 如果是 InfinityInfinity 相乘,则结果是 Infinity
console.log(
  NaN * 1, // NaN
  Infinity * Infinity, // Infinity
  Infinity * 1, // Infinity,
  Infinity * 0, // NaN
  
)
1
2
3
4
5
6
7

# 除法(/

  • 如果商超过了 ECMAScript 数值的表示范围,则返回 Infinity-Infinity
  • 如果有一个操作数是 NaN ,则结果是 NaN
  • 如果是 InfinityInfinity 除,则结果是 NaN
  • 如果是零被零除,则结果是 NaN
  • 如果是非零的有限数被零除,则结果是 Infinity-Infinity ,取决于有符号操作数的符号;
  • 如果是 Infinity 被任何非零数值除,则结果是 Infinity-Infinity ,取决于有符号操作数的符号;
console.log(
  NaN / 1, // NaN
  0 / 0, // NaN
  1 / 0, // Infinity
  Infinity / Infinity, // NaN
  Infinity / 1, // Infinity
  Infinity / 0, // Infinity
)
1
2
3
4
5
6
7
8

# 求模(求余数)(%

  • 如果被除数是有限大的数值而除数是零,则结果是 NaN
  • 如果被除数是 Infinity 而除数是有限大的数值,则结果是 NaN
  • 如果是 InfinityInfinity 除,则结果是 NaN
  • 如果被除数是有限大的数值而除数是无穷大的数值,则结果是被除数;
  • 如果被除数是零,则结果是零;
console.log(
  1 % 0, // NaN
  Infinity % 1, // NaN
  Infinity % Infinity, // NaN
  Infinity % 0, // NaN
  1 / Infinity, // 0
  0 % Infinity // 0
)
1
2
3
4
5
6
7
8

# 加性操作符

  • 加法(+
  • 减法(-

# 加法(+

  • 如果一侧为字符串,则对非字符串执行 String() 后,将两侧字符串拼接,若两侧为字符串则直接拼接。
  • 若一侧或两侧不是 string 也不是 number 类型,则对它执行 Number(param) 后再计算。param会转为相应的包装类型,然后先执行valueOf()、不行再执行 toString()操作。
  • 如果有一个操作数是 NaN ,则结果是 NaN
  • 如果是 InfinityInfinity ,则结果是 Infinity
  • 如果是 -Infinity-Infinity ,则结果是 -Infinity
  • 如果是 Infinity-Infinity ,则结果是 NaN
  • 如果是 +0+0,则结果是 +0
  • 如果是 -0-0,则结果是 -0
  • 如果是 +0-0,则结果是 +0
const obj = {
  valueOf () {
    return 1
  },
  toString () {
    return '1'
  }
}

console.log(
  1 + '23', // '123'
  1 + NaN, // NaN
  Infinity + Infinity, // Infinity
  Infinity + -Infinity, // NaN
  -Infinity + -Infinity, // -Infinity
  +0 + +0, // 0
  -0 + -0, // -0
  +0 + -0, // 0
  -0 + +0, // 0
  1 + true, // 2
  1 + undefined, // NaN
  1 + null, // 1
  1 + '', // '1' // 不会对 '' 执行转换,而是对 1 执行 String() 然后拼接字符串
  1 + {}, // '1[object Object]'
  1 + obj, // 2 // 先执行 obj 的 valueOf() 得到 1, 然后返回
  obj + obj, // 2
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 减法(-

  • 只要有一侧或两侧不是 string 类型,就对它执行 Number() 操作,然后再做计算。
  • 如果有一个操作数是 NaN ,则结果是 NaN
  • 如果是 InfinityInfinity ,则结果是 NaN
  • 如果是 -Infinity-Infinity ,则结果是 NaN
  • 如果是 Infinity-Infinity ,则结果是 Infinity
  • 如果是 -InfinityInfinity ,则结果是 -Infinity
  • 如果是 +0+0,则结果是 +0
  • 如果是 +0-0,则结果是 -0
  • 如果是 -0-0,则结果是 +0
console.log(
  1 - '23', // -22
  1 - NaN, // NaN
  Infinity - Infinity, // NaN
  -Infinity - -Infinity, // NaN
  Infinity - -Infinity, // Infinity
  +0 - +0, // 0
  -0 - -0, // 0
  +0 - -0, // 0
  -0 - +0, // -0
  1 - true, // 0
  1 - undefined, // NaN
  1 - null, // 1
  1 - '', // 1
  1 - {}, // NaN
  1 - obj // 0 // 先执行 obj 的 valueOf() 得到 1, 然后返回
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 关系操作符

关系操作符都返回一个布尔值。

  • 小于(<
  • 大于(>
  • 小于等于(<=
  • 大于等于(>=

比较规则:

  • 如果两个操作数都是 string 类型,则依次比较两个字符串对应位置的字符编码值。
  • 如果只有一侧是 number 类型,则另一侧无论什么类型都会执行 Number() 操作再做比较。
const obj = {
  valueOf () {
    return 0
  }
}
const obj2 = {}
console.log(
  'Bob' > 'andy', // false // B(66) 与 a(97) 比较,前者小,不满足表达式,停止比较返回 false
  'Bob' > 'Bo', // true // 前两位都相等,前者有最后一位后者没有,前者大。
  'Bobx' > 'Boby', // false // 前三位都相等, x(120) > y(121)
  '52' > '6', // false // 编码比较:5(53) 与 6(54),后者大,停止比较返回 false
  1 > '', // true  // Number('') 为 0
  1 > 'a', // false // Number(a) 为 NaN,NaN 与任何数比较都为 false
  1 > false, // true  // Number(false) 为 0
  1 > NaN, // false // NaN 与任何数比较都为 false
  1 > undefined, // false // Number(undefined) 为 NaN,NaN 与任何数比较都为 false
  1 > null, // true // Number(null) 为 0
  1 > obj, // true // Number(obj) 为 0
  1 > obj2, // false // Number(obj2) 为 '[object Object]',再执行 Number() 为 NaN
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

比较字母排序时,为防止大小写不能正常排序,应该先统一转成大写或小写再做排序。

# 相等操作符

# 相等和不相等(== / !=

  • 如果操作符两侧均为对象,不会发生转换,而是比较两侧是否指向同一个对象。
  • 如果操作符两侧均为 string 类型,不会发生转换,而是比较对应位置的字符编码。
  • 如果两侧不全为对象或不全为字符串,会对非数值(nullundefined 除外)执行 Number() 后再做比较。
  • nullundefined 是相等的,两者均不会被执行类型转换。
  • NaN 和任何值都不相等,包括 NaN 自身。
const obj = {
  a: 1,
  valueOf () {
    return 1
  }
}

const obj2 = obj
const obj3 = obj

console.log(
  obj2 == obj3, // true
  obj == true, // true // Number(obj) == Number(true)
  obj == '1', // true // Number(obj) == Number('1')
  obj == 1, // true // Number(obj) == 1
  true == '1', // true // Number(true) == Number('1')
  true == 1, // true // Number(true) == 1
  1 == '1', // true // 1 == Number('1')
  null == undefined, // true
  null == 0, // false // null 不会被执行 Number()
  NaN == NaN, // false
  NaN != NaN, // true
)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 全等和不全等(=== / !==

  • 不会对操作数执行任何转换,也就是必须 类型 都相等才返回 true
  • !== 操作符中,只要 类型 其中某一项不相等就返回 true

# 条件操作符(? :

var a = expression ? true_value : false_value

expression 表达式为 true,则式返回 true_value ,然后赋值给 a,否则返回 false_value 赋值给 a

# 赋值操作符(=

其作用就是把右侧的值赋给左侧的变量。

复合赋值操作符:

  • 乘/赋值( *= );
  • 除/赋值( /= );
  • 模/赋值( %= );
  • 加/赋值( += );
  • 减/赋值( -= );
  • 左移/赋值( <<= );
  • 有符号右移/赋值( >>= );
  • 无符号右移/赋值( >>>=

# 逗号操作符

使用逗号操作符可以在一条语句中执行多个操作。

// 声明多个变量

var num1=1, num2=2, num3=3;
1
2
3

在用于赋值时,逗号操作符总会返回表达式中的最后一项

var num = (5, 1, 4, 8, 0); // num 的值为 0
1

# 语句

# if 语句

if (condition) statement1 else statement2
1

condition 可以是任意表达式,如果不是 boolean 值,会对整个表达式执行 Boolean() 得到一个布尔值。这个值为 true 则执行 语句1(statement1),为 false 则执行 语句2(statement2)。这两个语句可以是一行代码,也可以是一个代码块(以一对花括号括起来的多行代码)。

推荐应该始终使用代码块的写法。

if (i > 1)
  console.log('> 1'); // 单行语句
else {
  console.log('< 1'); // 代码块中的语句
}
1
2
3
4
5
if (i > 10) {
  console.log('> 10');
} else if (i < 0) {
  console.log('< 0');
} else {
  console.log('> 0 && < 10')
}
1
2
3
4
5
6
7

# do-while 语句

do-while 语句是一种后测试循环语句,即只有在循环体中的代码执行之后,才会测试出口条件。换句话说,在对条件表达式求值之前,循环体内的代码至少会被执行一次。

最常用于循环体中的代码至少要被执行一次的情形。

do {
  statement
} while (expression);
1
2
3

下面是一个示例:

var i = 0;
do {
  i += 2;
} while (i < 10);

console.log(i); // 10

1
2
3
4
5
6
7

# while 语句

while 语句属于前测试循环语句,也就是说,在循环体内的代码被执行之前,就会对出口条件求值。因此,如果条件为 false,循环体内的代码永远不会被执行。

while (expression) {
  statement
}
1
2
3

下面是一个示例:

var i = 0;
while (i < 10) {
  i += 2;
}
console.log(i); // 10
1
2
3
4
5

# for 语句

for (initialization; expression; post-loop-expression) {
  statement
}
1
2
3
  • initialization 变量初始化
  • expression 为条件表达式,表达式为 true 才会执行循环语句
  • post-loop-expression 循环结束后对初始化变量的修改
var count = 10;
for (var i = 0; i < count; i++){
  console.log(i);
}

// 使用 while 语句 也可以实现

var count = 10;
var i = 0;
while (i < count) {
  console.log(i);
  i++;
}

// for 循环的另一种表示

var count = 10;
var i = 0;
for (; i < count; ){
  console.log(i);
  i++;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# for-in 语句

for-in 语句是一种精准的迭代语句,可以用来枚举对象的属性。

for (var key in obj) {
  statement
}
1
2
3
var obj = {
  a: 1,
  b: 2
}

for (var key in obj) {
  console.log(key, obj[key])
}
// a 1
// b 2
1
2
3
4
5
6
7
8
9
10

ES5 之前,迭代的对象 obj 值为 nullundefined 时 , for-in 语句会抛出错误。 ECMAScript 5 更正了这一行为;对这种情况不再抛出错误,而只是不执行循环体。

# label

使用 label 语句可以在代码中添加标签,以便将来使用。

start: for (var i = 0; i < count; i++) {
  console.log(i)
}
1
2
3

上例中的 start 标签可以在将来由 breakcontinue 语句引用。加标签的语句一般都 要与 for 语句等循环语句配合使用。

# break 和 continue 语句

break 语句会立即退出整个循环。continue 语句只是跳过本次循环,继续执行下一次循环。

breakcontinue 语句只能控制当前的一层循环,嵌套的循环中,不能控制外层循环,搭配 label 即可控制跳出任意一层循环。

for (var a = 1; a < 5; a++) {
  for (var b = 1; b < 5; b++) {
    if (a * b >= 6) {
      break
    }
    console.log(a, b)
  }
}
console.log('----------')
outer: for (var a = 1; a < 5; a++) {
  for (var b = 1; b < 5; b++) {
    if (a + b >= 6) {
      break outer
    }
    console.log(a, b)
  }
}
console.log('----------')
outer: for (var a = 1; a < 5; a++) {
  inner: for (var b = 1; b < 5; b++) {
    if (a * b >= 6) {
      break inner
    } else if (a + b >= 6) {
      break outer
    }
    console.log(a, b)
  }
}

/*
1 1
1 2
1 3
1 4
2 1
2 2
3 1
4 1
----------
1 1
1 2
1 3
1 4
2 1
2 2
2 3
----------
1 1
1 2
1 3
1 4
2 1
2 2
3 1
4 1
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

# with 语句

with 语句的作用是将代码的作用域设置到一个特定的对象中。

var qs = location.search.substring(1);
var hostName = location.hostname;
var url = location.href;
1
2
3

上面几行代码都包含 location 对象。如果使用 with 语句,可以把上面的代码改写成如下所示:

with (location) {
  var qs = search.substring(1);
  var hostName = hostname;
  var url = href;
}
1
2
3
4
5

严格模式下不允许使用 with 语句,否则将视为语法错误。

由于大量使用 with 语句会导致性能下降,同时也会给调试代码造成困难,因此 在开发大型应用程序时,不建议使用 with 语句。

# switch 语句

switch 语句与 if 语句的关系最为密切,而且也是在其他语言中普遍使用的一种流控制语句。

switch 语句在比较值时使用的是全等操作符,因此不会发生类型转换(例如, 字符串 "10" 不等于数值 10)。

switch (expression) {
  case value: statement
    break;
  case value: statement
    break;
  case value: statement
    break;
  case value: statement
    break;
  default: statement
}
1
2
3
4
5
6
7
8
9
10
11

switch 语句就是为了让开发人员免于编写过多的 if 语句。

合并几种情形的情况,刻意省略 break 语句(同时建议加上注释):

switch (i) {
  case 25:
    /*  合并两种情形 */
  case 35:
    console.log("25 or 35");
    break;
  case 45:
    console.log("45");
    break;
  default:
    console.log("Other");
}
1
2
3
4
5
6
7
8
9
10
11
12

switch 参数 和 case 值可以使用各种类型,包括表达式:

switch ("hello world") {
  case "hello" + " world":
    console.log("Greeting was found.");
    break;
  case "goodbye":
    console.log("Closing was found.");
    break;
  default:
    console.log("Unexpected message was found.");
}
1
2
3
4
5
6
7
8
9
10

switch 语句虽然是用于比较全等条件,但换一种写法也可以用作比较的条件:

var num = 25;
switch (true) {
  case num < 0:
    console.log("Less than 0.");
    break;
  case num >= 0 && num <= 10:
    console.log("Between 0 and 10.");
    break;
  case num > 10 && num <= 20:
    console.log("Between 10 and 20.");
    break;
  default:
    console.log("More than 20.");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 函数

ECMAScript 中的函数使用 function 关键字来声明,后跟一组参数以及函数体。 函数的基本语法如下所示:

function functionName(arg0, arg1,...,argN) {
  // statements
}
1
2
3

实例:

function sayHi(name, message) {
  console.log("Hello " + name + "," + message);
}

sayHi("Nicholas", "how are you today?");
1
2
3
4
5

可通过 return 语句后跟要返回的值来实现返回值。

function sum(num1, num2) {
  return num1 + num2;
}

console.log(sum(5, 10)); // 15
1
2
3
4
5

位于 return 语句之后的任何代码都永远不会执行。

function sum(num1, num2) {
  return num1 + num2;
console.log("Hello world"); //  永远不会执行
}
1
2
3
4

根据不同情况返回不同值:

function diff(num1, num2) {
  if (num1 < num2) {
    return num2 - num1;
  } else {
    return num1 - num2;
  }
}
1
2
3
4
5
6
7

函数在没有return 语句时,将默认返回 undefined, 在有 return 语句但没有带返回值时,依然返回 undefined,如果需要提前停止函数执行而又不需要返回值的情况下可使用 return;

function Func (num) {
  if (num < 0) {
    return;
  }
  console.log(num)
}
1
2
3
4
5
6

严格模式对函数有一些限制:

  • 不能把函数命名为 eval 或 arguments ;
  • 不能把参数命名为 eval 或 arguments ;
  • 不能出现两个命名参数同名的情况。

如果发生以上情况,就会导致语法错误,代码无法执行。

# 参数

ECMAScript 的函数参数不严格,即使定义接收2个参数,使用时也可以多传、少传或不传,

没有传递值的命名参数将自动被赋予 undefined 值。

原因是 ECMAScript 中的参数在内部是用一个数组来表示的。函数接收 到的始终都是这个数组,而不关心数组中包含哪些参数(如果有参数的话)。如果这个数组中不包含任 何元素,无所谓;如果包含多个元素,也没有问题。

实际上,在函数体内可以通过 arguments 对象来访问这个参数数组,从而获取传递给函数的每一个参数。

arguments 对象与数组类似(但它并不是 Array 的实例),仅是一个类数组对象。

  • 可以通过方括号语法访问元素: arguments[0]
  • 带有 length 属性可获取参数个数: arguments.length
function func () {
  console.log(arguments[0] + arguments[1])
  console.log(arguments.length)
}

func(1, 2)
1
2
3
4
5
6

这个事实说明了 ECMAScript 函数的一个重要特点:命名的参数只提供便利,但不是必需的。

arguments 的另一行为:它的值永远与对应命名参数的值保持同步。

function doAdd(num1, num2) {
  arguments[1] = 10; // 修改 arguments 中的值同时会作用到命名参数 num2 上
  console.log(num1 + num2)  
}

doAdd(3, 5); // 13
doAdd(3); // NaN

1
2
3
4
5
6
7
8

上例中,修改 arguments 中的值就相当于修改了 num2 ,但这并不是说读取这两个值会访问相同的内存空间;它们的内存空间是独立的,但它们的值会同步。

但在上例第二次调用中,只传入了一个参数,那么修改 arguments[1] 的值不会作用到对应的命名参数 num2。这是因为 arguments 对象的长度是由调用时传入的参数个数决定的,不是由定义函数时的命名参数的个数决定的。

严格模式对如何使用 arguments 对象做出了一些限制。

  • 像前面例子中那样的赋值会变得无效。也就是说,即使把 arguments[1] 设置为 10num2 的值仍然还是 undefined
  • 其次,重写 arguments 的值会导致语法错误(代码将不会执行)。

# 没有重载

ECMAScript 函数不能像传统意义上那样实现重载。而在其他语言(如 Java)中,可以为一个函数 编写两个定义,只要这两个定义的签名(接受的参数的类型和数量)不同即可。

如前所述,ECMAScirpt 函数没有签名,因为其参数是由包含零或多个值的数组来表示的。而没有函数签名,真正的重载是不可能做到的。

如果在 ECMAScript中定义了两个名字相同的函数,则该名字只属于后定义的函数。请看下面的例子:

function addSomeNumber(num){
  return num + 100;
}
function addSomeNumber(num) {
  return num + 200;
}
var result = addSomeNumber(100); //300
1
2
3
4
5
6
7

当然,通过检查传入函数中参数的类型和数量并作出不同的反应,可以模仿方法的重载。